#!/usr/bin/env python
"""
Class FeatureMatrix

 General purpose, iteratively-generated, memory-backed Feature Matrix class
 used to generate feature matrix files from, for example, DCC file
 processing. For information on invocation and usage, see the documentation for
 class FeatureMatrix, FeatureMatrix.__init__ and the remainder of this API,
 using, for example, help(FeatureMatrix) or help(FeatureMatrix.__init__).

 This class is built with repudibility of non-existent features in mind. For
 speculative entry of new features, a wrapper class or version that updates the
 internal indices will be required.

Author: Tom Robinson <trobinson@systemsbiology.org>
Initial Revision: January 11th, 2012

TODO:
* Support getters that copy the internal matrix and mask attributes
* Support cloning, recreation, __repr__, __str__, et cetera
* Support threading
* Make this more collection-like, supporting the standard method calls
* For jobs that eclipse system memory, provide an Apache Pig-backed version
* (Low priority) Profile linear space complexity over logarithmic sort step for
    cache perf reasons
* Documentation on the guarantee of preserving input row/column order 
* Make LINE_SEPARATOR part of FeatureMatrix.__init__(...)

"""

## Imports

import logging              ## Logging support 
import os                   ## OS-level operations 
import operator             ## For operator.itemgetter 

from random import shuffle  ## For random categorical element selection 


## Credit documentation

__author__ = "Tom Robinson"
__copyright__ = "Institute for Systems Biology, 2012"
__credits__ = ["Tom Robinson"]
__license__ = "Apache 2.0"
__version__ = "1.0"
__maintainer__ = "Tom Robinson"
__email__ = "trobinson@systemsbiology.org"
__status__ = "Production"
__platform__ = "Multi-OS, Python 2.6+"


## Constants
LINE_SEPARATOR = "\n"           ## Line separator: UNIX-style line breaks
BOOL_REPR = ['0','1']           ## String representation of boolean values
                                ##  after coercion, for the mask file


## Functions
def hasNumericalCast(given):
    """ Return whether the given variable can be cast into a numerical value
     (of both floating point and integer)


    Parameters
    ----------
    given : object
        Object for numerical identification

    Return Value
    ------------
    bool
        Whether this object can be considered numerical

    Examples
    --------
    >>> assert(hasNumericalCast(1.0) == True)

    """

    # Note: we want to pass both integer and float values through. We also wish 
    #  to validate that this object *CAN* be cast to such. Therefore, we use a
    #  try-except pattern on dynamic conversion instead of isinstance(...).
    try:
        float(given)
        int(given)
        return True
    except:
        pass


## Classes
class FeatureTypeHeuristic:
    CATEGORICAL_3PLUS   = -3    ## Three or more categories are present ## 
    CATEGORICAL_2       = -2    ## Two categories are present ##
    CATEGORICAL_1       = -1    ## One category was detected ##
    NEUTRAL             = 0     ## No data could be inferred ##
    NUMERICAL_1         = 1     ## One numerical feature was found ##
    NUMERICAL_2         = 2     ## Two numbers are present ##
    NUMERICAL_3PLUS     = 3     ## Three or more numbers are present ##


class FeatureMatrix:
    """ An immutably-sized feature matrix from a given list of row and
     column definitions, to be filled in iteratively during parsing. 

     See the documentation for __init__ for details on invocation.

     This class offers no special guarantees about standard representation,
     sorting, comparison, or the like. USE THEM AT YOUR OWN RISK.

     This class also provides no guarantees of thread safety.

    """
    _rowIdentifiers = None ## Row identifiers (see __init__) ##
    _colIdentifiers = None ## Column identifiers (see __init__) ##
    _separator = None      ## Column separator (see __init__) ##
    _emptyVal = None       ## The string representation of a missing value
                            ##      (see __init__) ##

    _matrix = None         ## The internal matrix representation ##
    _denom = None          ## The denominator, for numerical averaging ##
    _row1DMask = None      ## The internal (1D) row mask ##
    _col1DMask = None      ## The internal (1D) column mask ##
    _value2DMask = None    ## The internal (2D) value mask ##

    _row1DType = None      ## The inferred internal (1D) row type ##
    _col1DType = None      ## The inferred internal (1D) column type ##
    _row1DPrev = None      ## Internal row (1D) binary repudiation vector ##
    _col1DPrev = None      ## Internal col (1D) binary repudiation vector ##

    _logger = None         ## Logger to write any informative messages into
                            ##      (see __init__) ##


    def __init__(self, rowIdentifiers, colIdentifiers, separator="\t",
                     emptyVal="NA", logger=None):    
        """ Create an immutably-sized feature matrix from a given list of row
         and column definitions.

        Parameters
        ----------
        rowIdentifiers : list of string
            Row identifiers which ideally appear along the left column of the
            output feature matrix 

        colIdentifiers : list of string
            Column identifiers which ideally appear along the top row of the
            output feature matrix

        separator : string
            Column separator used when writing this matrix's output. 
            Default: "\t".

        emptyVal : string
            String used when no data is present in a cell. Default: "NA".

        logger : string
            Logger (from module "logging" ) passed in from the caller.
            Default: None.

        Return Value
        ------------
        FeatureMatrix
            Returns a new feature matrix to the given specification.

        Examples
        --------
        >>> fm = FeatureMatrix(['a','b','c'], ['x','y','z'])

        """

        len_rowIdentifiers = len(rowIdentifiers)
        len_colIdentifiers = len(colIdentifiers)

        # Convert our input list into a dictionary of identifier : location,
        #  for the lookup performance of a hash table.
        self._rowIdentifiers = dict([[rowIdentifiers[r],r] for r in \
                                      xrange(0,len_rowIdentifiers)])
        self._colIdentifiers = dict([[colIdentifiers[c],c] for c in \
                                      xrange(0,len_colIdentifiers)])

        # Special strings used during output processing
        self._separator = separator
        self._emptyVal = emptyVal
        self._logger = logger

        # Create the internal (2D) matrix as len(rows) x len(cols)
        self._matrix = [[None for c in range(len(self._colIdentifiers))] \
           for r in range(len(self._rowIdentifiers))]

        # Create the mask arrays. These are used for writing the output feature
        #  matrix and mask file.
        self._row1DMask = [False for r in xrange(0,len_rowIdentifiers)]
        self._col1DMask = [False for c in xrange(0,len_colIdentifiers)]
        self._val2DMask = {}

        # Create the internal arrays, for type autodetection.
        #self._row1DType = [FeatureTypeHeuristic.NEUTRAL \
        #  for r in xrange(0,len_rowIdentifiers)]
        self._col1DType = [FeatureTypeHeuristic.NEUTRAL \
          for c in xrange(0,len_colIdentifiers)]
        #self._row1DPrev = [[] for r in xrange(0,len_rowIdentifiers)]
        self._col1DPrev = [[] for c in xrange(0,len_colIdentifiers)]


    def set(self, rowIdentifier, colIdentifier, value, useAveraging=False):
        """ Set an element in the matrix to a specific value, inclusive of None

         This is done using name-based indexing, which will be used to identify
         the correct location in our internal matrix

        Parameters
        ----------
        rowIdentifier : string
            String identifier of the row to contain this value

        colIdentifier : string
            String identifier of the column to contain this value

        value : object
            Object to store at this location

        useAveraging : bool
            Specifies whether to use averaging behavior when the same cell in
            the matrix is set multiple times. Default: True.

        Return Value
        ------------
        None (the given value is set internally)

        Examples
        --------
        >>> fm = FeatureMatrix(["Deep Thought"], ["The Answer"])
        >>> fm.set("Deep Thought", "The Answer", 42)

        """

        # If an entry is written to multiple times, we perform numerical and
        #  categorical averaging
        #
        #  Numerical averaging is done by keeping a rolling denominator. 
        #  Categorical averaging is done by selecting one of the two categories
        #   at random, giving each category a shot at being selected. 
        #
        #  Because categorical averaging results in non-determinism, we perform
        #  it... then throw an exception that we expect to be handled, because
        #  we're not comfortable with this routine at all. 

        # Resolve averaging, if any needs to be performed
        usedCategoricalAveraging = False
        r = self._rowIdentifiers[rowIdentifier]
        c = self._colIdentifiers[colIdentifier]
        if self._matrix[r][c] != None:
            if self._logger:
                if useAveraging:
                    raise NotImplementedError( \
                      "Averaging is not available in" +
                      " this version of fmatrix.py")
                    #self._logger.warn( \
                    #  '%s : %s was already present in matrix! Averaging...' % \
                    #  (rowIdentifier,colIdentifier))
                else:
                    self._logger.warn( \
                      '%s : %s was already present in matrix! Replacing...' % \
                      (rowIdentifier,colIdentifier))

        if useAveraging and self._matrix[r][c] != None:

            self._denom[(self._rowIdentifiers[rowIdentifier], \
              self._colIdentifiers[colIdentifier])] += 1

            try:
                if value != self._matrix[r][c]:
                    value += float(self._matrix[r][c])
            except:
                selections = [value,self._matrix[r][c]]
                shuffle(selections)
                value = selections[0]
                usedCategoricalAveraging = True

                if self._logger:
                    self._logger.warn( \
                      "Performing categorical averaging. This is" \
                      + " non-deterministic and not recommended!")
        else:

            # Do a quick lookup for this column to verify the value is new
            if len(self._col1DPrev[c]) < 3 and value not in self._col1DPrev[c]:
                
                self._col1DPrev[c].append(value)
                colType = self._col1DType[c]
    
                if hasNumericalCast(value) and colType >= 0:

                    # Only parse numerically in the non-presence of categorical
                    #  information 
                    if colType != 3:
                        colType += 1

                else:
                    # Invert numerical features when a categorical one is found
                    if colType > 0:
                        colType *= -1

                    # Signal an additional categorical feature
                    if colType != -3:
                         colType -= 1

                # Set the new type
                self._col1DType[c] = colType

        # Allow any exceptions generated here to propagate to the top level
        self._matrix[r][c] = value

        if usedCategoricalAveraging:
            raise ValueError("Categorical averaging requires explicit" \
              + " handling of this exception to proceed. " \
              + " See FeatureMatrix.set(...) for more details.")


    def get(self, rowIdentifier, colIdentifier):
        """ Get an element in the matrix from a specific location

         Useful for debugging and checking for duplicate values

        Parameters
        ----------
        rowIdentifier : string
            String identifier of the row to contain this value

        colIdentifier : string
            String identifier of the column to contain this value

        Return Value
        ------------
        object
            The object set at this location

        Examples
        --------
        >>> fm = FeatureMatrix(["Arthur Dent"], ["Location in the Galaxy"])
        >>> fm.set("Arthur Dent", "Location in the Galaxy", "Earth")
        >>> assert(fm.get("Arthur Dent", "Location in the Galaxy") == "Earth")

        """

        if self._matrix[self._rowIdentifiers[rowIdentifier]] \
            [self._colIdentifiers[colIdentifier]] != None:
            if self._logger:
                self._logger.warn(
                  '%s : %s could not be found in matrix. Returning None.' % \
                  (rowIdentifier,colIdentifier))
            return None

        # Allow any exceptions generated here to propagate to the top level
        return self._matrix[ \
            self._rowIdentifiers[rowIdentifier]][ \
            self._colIdentifiers[colIdentifier]]


    def setVal2DMask(self, rowIdentifier, colIdentifier, value):
        """ Set a value mask to a specific boolean value, NOT inclusive of None

        Parameters
        ----------
        rowIdentifier : string
            String identifier of the row to contain this value

        colIdentifier : string
            String identifier of the column to contain this value

        value : bool
            Boolean value to store at this location

        Return Value
        ------------
        None (the given value is set internally)

        Examples
        --------
        >>> fm = FeatureMatrix(["Deep Thought"], ["The Question"])
        >>> fm.setVal2DMask("Deep Thought", "The Question", True)

        """

        # Sanity check
        if not isinstance(value,bool):
            raise ValueError('The supplied value was not a boolean')

        # Allow any exceptions generated here to propagate to the top level
        self._val2DMask[ \
            (self._rowIdentifiers[rowIdentifier], \
            self._colIdentifiers[colIdentifier])] \
        = value


    def getVal2DMask(self, rowIdentifier, colIdentifier):
        """ Get the mask bit from a specific location

        Parameters
        ----------
        rowIdentifier : string
            String identifier of the row to contain this value

        colIdentifier : string
            String identifier of the column to contain this value

        Return Value
        ------------
        bool
            The mask value at this location

        Examples
        --------
        >>> fm = FeatureMatrix(["The Galaxy"], ["Earth, after the Vogons"])
        >>> fm.setVal2DMask("The Galaxy", "Earth, after the Vogons", True)
        >>> assert( \
          fm.getVal2DMask("The Galaxy", "Earth, after the Vogons") == True)

        """

        # Allow any exceptions generated here to propagate to the top level        
        if (self._rowIdentifiers[rowIdentifier], \
            self._colIdentifiers[colIdentifier]) not in self._val2DMask:
            return False

        return self._val2DMask[ \
            (self._rowIdentifiers[rowIdentifier], \
            self._colIdentifiers[colIdentifier])]


    def setRow1DMask(self, rowIdentifier, value):
        """ Set a row mask to a specific boolean value, NOT inclusive of None

        Parameters
        ----------
        rowIdentifier : string
            String identifier of the row to contain this value

        value : bool
            Boolean value to store at this location

        Return Value
        ------------
        None (the given value is set internally)

        Examples
        --------
        >>> fm = FeatureMatrix( \
            ["So Long, and Thanks for All the Fish"], [])
        >>> fm.setRow1DMask("So Long, and Thanks for All the Fish", False)

        """

        # Sanity check
        if not isinstance(value,bool):
            raise ValueError('The supplied value was not a boolean')

        # Allow any exceptions generated here to propagate to the top level
        self._row1DMask[self._rowIdentifiers[rowIdentifier]] = value


    def getRow1DMask(self, rowIdentifier):
        """ Get the mask bit for this 1D row identifier

        Parameters
        ----------
        rowIdentifier : string
            String identifier of the row to contain this value

        Return Value
        ------------
        bool
            The mask value at this location

        Examples
        --------
        >>> fm = FeatureMatrix(["Petunias"], [])
        >>> fm.setRow1DMask("Petunias", False)
        >>> assert(fm.getRow1DMask("Petunias") == False)

        """

        # Allow any exceptions generated here to propagate to the top level
        return self._row1DMask[self._rowIdentifiers[rowIdentifier]]


    def setCol1DMask(self, colIdentifier, value):
        """ Set a column mask to a specific boolean value, NOT inclusive of None

        Parameters
        ----------
        colIdentifier : string
            String identifier of the column to contain this value

        value : bool
            Boolean value to store at this location

        Return Value
        ------------
        None (the given value is set internally)

        Examples
        --------
        >>> fm = FeatureMatrix([], ["The Origins of The Guide"])
        >>> fm.setCol1DMask("The Origins of The Guide", True)

        """

        # Sanity check
        if not isinstance(value,bool):
            raise ValueError('The supplied value was not a boolean')

        # Allow any exceptions generated here to propagate to the top level
        self._col1DMask[self._colIdentifiers[colIdentifier]] = value
 

    def getCol1DMask(self, colIdentifier):
        """ Get the mask bit for this 1D column identifier

        Parameters
        ----------
        colIdentifier : string
            String identifier of the column to contain this value

        Return Value
        ------------
        bool
            The mask value at this location

        Examples
        --------
        >>> fm = FeatureMatrix([], ["Whales in Space"])
        >>> fm.setCol1DMask("Whales in Space", True)
        >>> assert(fm.getCol1DMask("Whales in Space") == True)

        """

        # Allow any exceptions generated here to propagate to the top level
        return self._col1DMask[self._colIdentifiers[colIdentifier]]


    def clear(self):
        """ Clear the internal matrix and masks

        Parameters
        ----------
        None

        Return Value
        ------------
        None (the internal matrix and masks are cleared)

        Examples
        --------
        >>> fm = FeatureMatrix([], [])
        >>> fm.clear()  # Our matrix and masks are now cleared

        """

        len_rowIdentifiers = len(self._rowIdentifiers)
        len_colIdentifiers = len(self._colIdentifiers)

        # Revert the internal matrix
        for r in xrange(0,len_rowIdentifiers):
            for c in xrange(0,len_colIdentifiers):
                self._matrix[r][c] = None

        # Revert the mask arrays
        for r in xrange(0,len_rowIdentifiers):
            self._row1DMask[r] = False

        for c in xrange(0,len_colIdentifiers):
            self._col1DMask[c] = False

        self._val2DMask = {}

        # Revert the type identifiers
        #for r in xrange(0,len_rowIdentifiers):
        #    self._row1DType[r] = FeatureTypeHeuristic.NEUTRAL
        #    self._row1DPrev[r] = []

        for c in xrange(0,len_colIdentifiers):
            self._col1DType[c] = FeatureTypeHeuristic.NEUTRAL
            self._col1DPrev[c] = []


    def _getVirtualMapping(self, removeMaskedRows, removeMaskedCols, transpose):
        """ Get the virtually-mapped representation of our output rows and
        columns, with additional details, for writing in default or transposed
        formats 

         * Internal method: doctests rolled into dependent methods

        Parameters
        ----------
        removeMaskedRows : bool
            Specifies whether to remove rows masked by the appropriate 1D
            array

        removeMaskedCols : bool
            Specifies whether to remove columns masked by the appropriate 1D
            array 

        transpose : bool
            Specifies whether to transpose the matrix (horizontal columns,
            vertical rows) instead of the default row/column view

        Return Value
        ------------
        tuple of virtually-reordered row and column data, including attributes

        Examples
        --------
        self._getVirtualMapping(True, True, False) # internally-reordered row
                                                    #  and column data 

        """
        # Re-sort our row identifiers
        sorted_rowIdentifiers = sorted(self._rowIdentifiers.items(), \
            key=operator.itemgetter(1))

        # Re-sort the column identifiers and add the data type identifiers
        sorted_colIdentifiers = sorted(self._colIdentifiers.items(), \
          key=operator.itemgetter(1))
        for h in sorted_colIdentifiers:

            # Map the type identifier
            featureType = None            

            if self._col1DType[h[1]] == \
              FeatureTypeHeuristic.CATEGORICAL_3PLUS:
                featureType = 'C:'

            elif self._col1DType[h[1]] == \
              FeatureTypeHeuristic.NUMERICAL_3PLUS:
                featureType = 'N:'

            else:
                featureType = 'B:'

            # Prepend it to each feature as a component of its identifier
            sorted_colIdentifiers[h[1]] = (featureType + h[0],h[1])

        # Remap our row and column identifiers into their virtual
        #  representations, depending on whether we use native or transpose
        #  order (in 2D space).
        if not transpose:
            return (sorted_rowIdentifiers, self._row1DMask, removeMaskedRows,\
              sorted_colIdentifiers, self._col1DMask, removeMaskedCols)
        else:
            return (sorted_colIdentifiers, self._col1DMask, removeMaskedCols,\
              sorted_rowIdentifiers, self._row1DMask, removeMaskedRows)

        

    def writeFeatureMatrix(self, filename, removeMaskedRows=True, 
                            removeMaskedCols=True, 
                            treatMaskedRowsAsNA=False,
                            treatMaskedColsAsNA=False,
                            treatMasked2DAsNA=True, 
                            transpose=False, 
                            clobber=False): 

        """ Write a usable feature matrix with mask settings applied

         This function provides for the ability to write an unmasked feature
         matrix (that is, one where all set values are displayed) or a feature
         matrix with some combination of 1D or 2D masking applied. This
         function also provides for the ability to replace masked values with
         NA instead, rendering the mask file unnecessary.

         Where applicable, removal of rows and columns takes precedence over
         masking of any values with emptyVal (that is, our internal
         representation of "NA"). This is because if these cells are omitted,
         their value would be pointless anyway.

        Parameters
        ----------
        filename : string
            Canonical path to write this feature matrix into.

        removeMaskedRows : bool
            Specifies whether to remove rows masked by the appropriate 1D
            array

        removeMaskedCols : bool
            Specifies whether to remove columns masked by the appropriate 1D
            array 

        treatMaskedRowsAsNA : bool
            Specifies whether to treat rows masked by the appropriate 1D array 
            as emptyVal ("NA" or equivalent)

        treatMaskedColsAsNA : bool
            Specifies whether to treat columns masked by the appropriate 1D
            array as emptyVal ("NA" or equivalent)

        treatMasked2DAsNA : bool
            Specifies whether to treat masked (2D) cells as emptyVal ("NA" or
            equivalent) 

        transpose : bool
            Specifies whether to transpose the matrix (horizontal columns,
            vertical rows) instead of the default row/column view

        clobber : bool
            Specifies whether to clobber the file at the target location if it
            already exists

        Return Value
        ------------
        None (the matrix file is written to disk if an exception is not raised)

        Examples
        --------
        >>> fm = FeatureMatrix(["Dolphin Departure Acronym"], ["SLaTfAtF"])
        >>> fm.writeFeatureMatrix('/dev/null',clobber=True) # Write our matrix

        """

        if self._logger: 
            self._logger.info( \
             'writeFeatureMatrix: attempting to write %dx%d matrix into %s'\
             % (len(self._rowIdentifiers), \
                len(self._colIdentifiers), \
                filename))

        # Check if the target already exists
        if os.path.exists(filename):
            if not clobber:
                raise IOError( \
                    'Clobber not set and matrix target %s already exists' % \
                    filename)
            if os.path.isdir(filename):
                raise IOError( \
                    'Matrix target %s appears to be a directory' % \
                     filename)

        # Open the target file
        fhandle = open(filename, 'w')

        # Get our virtual row and column data
        (virtualRowIdentifiers, virtualRow1DMask, virtualRemoveMaskedRows, \
          virtualColIdentifiers, virtualCol1DMask, virtualRemoveMaskedCols) = \
          self._getVirtualMapping(removeMaskedRows,removeMaskedCols,transpose)

        # Write the output row header
        fhandle.write('.') 
            # !! LEGACY NOTE: some tools fail to parse FMs with an empty
            #  cell here, so we fill it with a dot.
        for h in virtualColIdentifiers:
            fhandle.write(self._separator)
            fhandle.write(h[0])
        fhandle.write(LINE_SEPARATOR)

        # Write our matrix, relative to its virtual components
        for r in xrange(0,len(virtualRowIdentifiers)):
        
            # Skip this row entirely if requested
            if virtualRemoveMaskedRows and virtualRow1DMask[r] == True:
                continue

            fhandle.write(virtualRowIdentifiers[r][0])
            for c in xrange(0,len(virtualColIdentifiers)):
                # Skip this entry if requested
                if virtualRemoveMaskedCols and virtualCol1DMask[c]:
                    continue

                # Remap r and c back to their literal values
                x,y = r,c
                if transpose:
                    x,y = c,r

                # Write emptyVal to this region if this is requested 
                if (treatMaskedRowsAsNA and self._row1DMask[x] == True) or \
                    (treatMaskedColsAsNA and self._col1DMask[y] == True) or \
                    (treatMasked2DAsNA and (x,y) in self._val2DMask and \
                    self._val2DMask[(x,y)] == True): 
                        fhandle.write(self._separator)
                        fhandle.write(self._emptyVal)
                        continue
                
                if self._matrix[x][y] != None:
                    fhandle.write(self._separator)

                    # Attempt numerical averaging. If that fails, assume this
                    #  is simply categorical and treat it as such.
                    val = None
                    try: #if hasNumericalCast(val):
                        val = "%.2f" % self._matrix[x][y]
                    except:
                        val = str(self._matrix[x][y])

                    fhandle.write(val)
                else:
                    fhandle.write(self._separator)
                    fhandle.write(self._emptyVal)

            # Finish writing this line by writing our line separator
            fhandle.write(LINE_SEPARATOR)
    
        # Close the file
        fhandle.close()

        # Inform our logger
        if self._logger:
            self._logger.info('writeFeatureMatrix: Done writing %s' % \
                filename) 


    def writeMaskFile(self, filename, removeMaskedRows=True,
                       removeMaskedCols=True, transpose=False, clobber=False):
        """ Write the 2D mask file used to filter out individual values

         The mask file is used by tools such as RF-ACE to treat individual
         values as "NA". This was created primarily as a workaround for cases
         where a modular mask was preferable over a generated feature matrix
         file that contains NAs in these locations instead. 

         As such, this may be considered legacy in lieu of
         writeFeatureMatrix(... treat2DMaskedAsNA=True), if the implementer so
         wishes.

        Parameters
        ----------
        filename : string
            Canonical path to write this feature matrix into.

        removeMaskedRows : bool
            Specifies whether to remove rows masked by the appropriate 1D
            array (to maintain size parity with the output feature matrix)

        removeMaskedCols : bool
            Specifies whether to remove columns masked by the appropriate 1D
            array (to maintain size parity with the output feature matrix)

        transpose : bool
            Specifies whether to transpose the matrix (horizontal columns,
            vertical rows) instead of the default row/column view

        clobber : bool
            Specifies whether to clobber the file at the target location if it
            already exists

        Return Value
        ------------
        None (the mask file is written to disk if an exception is not raised)

        Examples
        --------
        >>> fm = FeatureMatrix(["The Last Message"], \
            ["We apologize for the inconvenience"])
        >>> fm.writeMaskFile('/dev/null',clobber=True)  # Write our mask file

        """ 
        
        if self._logger: 
            self._logger.info( \
             'writeFeatureMatrix: attempting to write %dx%d mask file into %s'\
             % (len(self._rowIdentifiers), \
                len(self._colIdentifiers), \
                filename))

        # Check if the target already exists
        if os.path.exists(filename):
            if not clobber:
                raise IOError( \
                    'Clobber not set and mask file target %s already exists' % \
                     filename)
            if os.path.isdir(filename):
                raise IOError( \
                    'Mask file target %s appears to be a directory' % \
                     filename)

        # Open the target file
        fhandle = open(filename, 'w')        

        # Get our virtual row and column data
        (virtualRowIdentifiers, virtualRow1DMask, virtualRemoveMaskedRows, \
          virtualColIdentifiers, virtualCol1DMask, virtualRemoveMaskedCols) = \
          self._getVirtualMapping(removeMaskedRows,removeMaskedCols,transpose)

        # Write the output row header
        fhandle.write('.') 
            # !! LEGACY NOTE: some tools fail to parse FMs with an empty
            #  cell here, so we fill it with a dot.
        for h in virtualColIdentifiers:
            fhandle.write(self._separator)
            fhandle.write(h[0])
        fhandle.write(LINE_SEPARATOR)

        # Iterate our matrix, writing its values (masked or not)
        for r in xrange(len(virtualRowIdentifiers)):
        
            # Skip this row entirely if requested
            if virtualRemoveMaskedRows and virtualRow1DMask[r] == True:
                continue

            fhandle.write(virtualRowIdentifiers[r][0])
            for c in xrange(0,len(virtualColIdentifiers)):

                # Skip this entry if requested
                if virtualRemoveMaskedCols and virtualCol1DMask[c]:
                    continue

                # Remap r and c back to their literal values
                x,y = r,c
                if transpose:
                    y,x = c,r

                # Write the mask
                fhandle.write(self._separator)
                fhandle.write( \
                  str(BOOL_REPR[(x,y) in self._val2DMask and \
                  self._val2DMask[(x,y)]]))

            # Finish writing this line by writing our line separator
            fhandle.write(LINE_SEPARATOR)
    
        # Close the file
        fhandle.close()
        if self._logger:
            self._logger.info('writeMaskFile: Done writing %s' % \
                filename)


# Invoke our inline doctests when this library is invoked directly
if __name__ == "__main__":
    import doctest
    doctest.testmod()
